#include "winsys.h"
#include <assert.h>
#include <map>

static std::map<HWND, CWindow*> g_HWNDCWindowMap;

// CWindow impl

CWindow::CWindow() : m_hWnd(nullptr) {
	SetHWND(nullptr);
}

CWindow::CWindow(HWND hWnd) : m_hWnd(hWnd) {
	SetHWND(hWnd);
}

CWindow::~CWindow() {
	if(m_hWnd) DestroyWindow(m_hWnd);
	SetHWND(nullptr);
}

HWND CWindow::GetHWND() { return m_hWnd; }
const HWND CWindow::GetHWND() const { return m_hWnd; }

int CWindow::SetHWND(HWND hWnd) {
	assert(GetCWindow(hWnd) == nullptr);
	
	g_HWNDCWindowMap[m_hWnd] = nullptr;
	m_hWnd = hWnd;
	g_HWNDCWindowMap[m_hWnd] = this;
	return 0;
}

CWindow* GetCWindow(HWND hWnd) {
	if(hWnd == nullptr) return nullptr;

	auto it = g_HWNDCWindowMap.find(hWnd);
	if(it == g_HWNDCWindowMap.end()) return nullptr;
	else return it->second;
}



// CLayoutWindow impl

CLayout::CLayout() : m_TargetWindow(nullptr) {
	m_Rect.left   = 0;
	m_Rect.right  = 0;
	m_Rect.top    = 0;
	m_Rect.bottom = 0;
}

CLayout::~CLayout() {
	Clear();
}

CLayout::SPosValue CLayout::CreatePixelPosValue(int pixel) {
	if(pixel < 0) pixel = 0;

	SPosValue pos;
	pos.postype = POSITION_PIXEL;
	pos.value.pixel = pixel;
	return pos;
}


CLayout::SPosValue CLayout::CreateRatioPosValue(float ratio) {
	if(ratio > 1) ratio = 1;
	else if(ratio < 0) ratio = 0;

	SPosValue pos;
	pos.postype = POSITION_RATIO;
	pos.value.ratio = ratio;
	return pos;
}


void CLayout::SetApplyTarget(CWindow* target) {
	m_TargetWindow = target;
}


void CLayout::SetRect(RECT rt) {
	m_Rect = rt;
}



void CLayout::Clear() {
	//free layouts. windows will be automatically be deleted by operating system.
	for(auto it = m_WindowTable.begin() ; it != m_WindowTable.end() ; it++) {
		if(it->m_ItemType == ITEM_LAYOUT) {
			delete it->m_Data.m_Layout;
		}
		else {
			delete it->m_Data.m_Wnd;
		}
	}
}


HLAYOUTITEM CLayout::CreateItem() {
	SLayoutItem obj;
	obj.m_ItemType = ITEM_WINDOW;
	obj.m_Dir = APPEND_UP;
	obj.m_Data.m_Wnd = nullptr;
	obj.m_Size = CLayout::CreatePixelPosValue(0);
	obj.m_Margin = 0;

	m_WindowTable.push_back(obj);

	auto it = m_WindowTable.end();
	it--;
	return it;
}


HLAYOUTITEM CLayout::SetWindow(HLAYOUTITEM orig, EAppendDir dir, CWindow* subwindow, SPosValue size, int margin) {
	/**/ if(orig->m_ItemType == ITEM_WINDOW) delete orig->m_Data.m_Wnd;
	else if(orig->m_ItemType == ITEM_LAYOUT) delete orig->m_Data.m_Layout;

	SLayoutItem& obj = *orig;
	obj.m_ItemType = ITEM_WINDOW;
	obj.m_Dir = dir;
	obj.m_Data.m_Wnd = subwindow;
	obj.m_Size = size;
	obj.m_Margin = margin;

	return orig;
}


HLAYOUTITEM CLayout::SetLayout(HLAYOUTITEM orig, EAppendDir dir, CLayout* sublayout, SPosValue size, int margin) {
	/**/ if(orig->m_ItemType == ITEM_WINDOW) delete orig->m_Data.m_Wnd;
	else if(orig->m_ItemType == ITEM_LAYOUT) delete orig->m_Data.m_Layout;

	SLayoutItem& obj = *orig;
	
	obj.m_ItemType = ITEM_LAYOUT;
	obj.m_Dir = dir;
	obj.m_Data.m_Layout = sublayout;
	obj.m_Size = size;
	obj.m_Margin = margin;
	
	return orig;
}


HLAYOUTITEM CLayout::SetGrid(HLAYOUTITEM orig, EAppendDir dir, CWindow** subWindows, int nHorizontal, 
	int nVertical, SPosValue size, int margin, int submargin) {

	//helper function.
	CLayout *sublayout = new CLayout(); // layout for holding grid items
	CWindow** wptr = subWindows;						// iterator for subWindows

	SPosValue full = CreateRatioPosValue(1.0f);		// 100% width/height

	// For every row, we create a layout and append subwindows to a row,
	//  and append row layout to sublayout.
	// Generate & insert nVertical rows to sublayout
	for(int y = 0 ; y < nVertical ; y++) {
		SPosValue subh = CreateRatioPosValue(1.0f / (float)(nVertical - y));
			// nVertical - y row should be inserted.
			// So this row holds 1/(nVertical - y) of remaining working area.

		CLayout *rowwnd = new CLayout();	// row layout

		// Insert nHorizontal elements to row layout
		for(int x = 0 ; x < nHorizontal ; x++) {
			SPosValue subw = CreateRatioPosValue(1.0f / (float)(nHorizontal - x));
			rowwnd->SetWindow(rowwnd->CreateItem(), APPEND_LEFT, *wptr, subw, submargin);
			wptr++;
		}

		// Insert row layout to sublayout.
		sublayout->SetLayout(sublayout->CreateItem(), APPEND_UP, rowwnd, subh, 0);
	}
	
	// sublayout generated. Append sublayout.
	SetLayout(orig, dir, sublayout, size, margin);

	return orig;
}


void CLayout::GetLayoutItemPos(HLAYOUTITEM _item, EAppendDir   *dir, SPosValue   *size, int   *margin) const {
	SLayoutItem* item = &*_item;
	if(dir)    *dir = item->m_Dir;
	if(size)   *size = item->m_Size;
	if(margin) *margin = item->m_Margin;
}

void CLayout::SetLayoutItemPos(HLAYOUTITEM _item, EAppendDir newdir, SPosValue newsize, int newmargin) {
	SLayoutItem* item = &*_item;
	item->m_Dir = newdir;
	item->m_Size = newsize;
	item->m_Margin = newmargin;
}


void CLayout::DeleteLayoutItem(HLAYOUTITEM _item) {
	m_WindowTable.erase(_item);
}
	


bool CLayout::IsResizable(int w, int h) const {
	int minw, minh;
	GetMinimumSize(&minw, &minh);
	return minw <= w && minh <= h;
}


void CLayout::GetMinimumSize(int *w, int *h) const {
	//NOTE : Don't cache the output!
	// User may change interior, and there is some cases where
	//  the changes cannot be detected by CLayout's interface.
	//  (ex : changes in sublayout : no method to propagate change,
	//   and even if there is, it makes the design very complicated
	//   compared to speed benefit.)
	// Nobody resizes the window frequently for the need of
	// caching. This raw version is fast enough.

	int minw, minh; // Minimum size required are calculated here
	minw = minh = 0;

	if(m_WindowTable.size() > 0) { // no object -> size = 0
		// Here, we build up the working area from the bottom.
		for(auto it = m_WindowTable.rbegin() ; it != m_WindowTable.rend() ; it++) {
			int subminow, subminoh; // minimum size of objects.
			int subminrw, subminrh; // minimum size of subregion.
			bool ah = false;		// object appended in horizontal direction?

			//get minimum size of object
			switch(it->m_ItemType) {
			case ITEM_LAYOUT:
				it->m_Data.m_Layout->GetMinimumSize(&subminow, &subminoh);
				break;

			case ITEM_WINDOW:
				subminow = subminoh = 10;
				break;
			}

			subminow += 2 * it->m_Margin;
			subminoh += 2 * it->m_Margin;

			//get minimum size of subregion. (where object takes space)
			switch(it->m_Dir) {
			case APPEND_LEFT:
			case APPEND_RIGHT:
				if(it->m_Size.postype == POSITION_PIXEL) subminrw = it->m_Size.value.pixel;
				else {
					assert(minw != 0 || it->m_Size.value.ratio >= 0.999f);
					if(minw == 0) subminrw = 50;
					else subminrw = (int)(minw * it->m_Size.value.ratio / (1 - it->m_Size.value.ratio));
					if(subminrw < 50) subminrw = 50;
						// Thought that at least 50 pixel is needed for ratio-controlled region
						// If you need a smaller region, use POSITION_PIXEL
				}

				subminrh = minh;
				ah = true;
				break;

			case APPEND_UP:
			case APPEND_DOWN:
				if(it->m_Size.postype == POSITION_PIXEL) subminrh = it->m_Size.value.pixel;
				else {
					assert(minh != 0 || it->m_Size.value.ratio >= 0.999f);
					if(minh == 0) subminrh = 50;
					else subminrh = (int)(minh * it->m_Size.value.ratio / (1 - it->m_Size.value.ratio));
					if(subminrh < 50) subminrh = 50;
				}

				subminrw = minw;
				ah = false;
				break;
			}

			//adjust the miminal size of the subregion using minimal object size
			subminrw = max(subminrw, subminow);
			subminrh = max(subminrh, subminoh);
		
			//apply to minw, minh
			switch(it->m_Size.postype) {
			case POSITION_PIXEL:
				if(ah) {
					minw += subminrw;
					minh = max(minh, subminrh);
				}
				else {
					minh += subminrh;
					minw = max(minw, subminrw);
				}
				break;

			case POSITION_RATIO:
				if(ah) {
					minw = (int)(subminrw / it->m_Size.value.ratio);
					minh = max(minh, subminrh);
				}
				else {
					minh = (int)(subminrh / it->m_Size.value.ratio);
					minw = max(minw, subminrw);
				}
			}
		}
	}

	//minw, minh computed.
	*w = minw;
	*h = minh;
}


void CLayout::UpdatePos() {
	// working area. remaining windows are placed here.
	int rectx, recty, rectw, recth;
	rectx = m_Rect.left; recty = m_Rect.top;
	rectw = m_Rect.right  - m_Rect.left;
	recth = m_Rect.bottom - m_Rect.top ;

	// iterate windows through reverse order of appended order
	// working area will change through every insertion.
	for(auto it = m_WindowTable.begin() ; it != m_WindowTable.end() ; it++) {
		// calculate subwindow's size
		int subw, subh;
		
		switch(it->m_Dir) {
		case APPEND_LEFT:
		case APPEND_RIGHT:
			if(it->m_Size.postype == POSITION_PIXEL) subw = it->m_Size.value.pixel;
			else subw = (int)((float)rectw * it->m_Size.value.ratio + 0.5f);
			subh = recth;
			break;

		case APPEND_UP:
		case APPEND_DOWN:
			if(it->m_Size.postype == POSITION_PIXEL) subh = it->m_Size.value.pixel;
			else subh = (int)((float)recth * it->m_Size.value.ratio + 0.5f);
			subw = rectw;
			break;
		}

		// calculate subwindow's position relative to remaining working area
		int insertx, inserty;
		switch(it->m_Dir) {
		case APPEND_LEFT:
		case APPEND_UP :
			insertx = 0;
			inserty = 0;
			break;

		case APPEND_DOWN:
			insertx = 0;
			inserty = recth - subh;
			break;

		case APPEND_RIGHT:
			insertx = rectw - subw;
			inserty = 0;
			break;
		}


		// Move!
		switch(it->m_ItemType) {
		case ITEM_WINDOW:
			if(it->m_Data.m_Wnd->GetHWND()) {
				MoveWindow(it->m_Data.m_Wnd->GetHWND(), 
					rectx + insertx + it->m_Margin,
					recty + inserty + it->m_Margin,
					subw - 2 * it->m_Margin,
					subh - 2 * it->m_Margin,
					TRUE);
			}
			break;

		case ITEM_LAYOUT:
			{
				RECT rt;
				rt.left   = rectx + insertx + it->m_Margin;
				rt.top    = recty + inserty + it->m_Margin;
				rt.right  = rt.left + subw - 2 * it->m_Margin;
				rt.bottom = rt.top  + subh - 2 * it->m_Margin;
				it->m_Data.m_Layout->SetRect(rt);
				it->m_Data.m_Layout->UpdatePos();
			}
			break;
		}

		// update working area
		switch(it->m_Dir) {
		case APPEND_LEFT:	rectx += subw;	rectw -= subw;	break;
		case APPEND_UP:		recty += subh;	recth -= subh;	break;
		case APPEND_DOWN:	recth -= subh;	break;
		case APPEND_RIGHT:	rectw -= subw;	break;
		}
	}
}